// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <fbl/algorithm.h>
#include <fbl/unique_fd.h>
#include <fuchsia/io/c/fidl.h>
#include <lib/fzl/fdio.h>
#include <lib/zx/handle.h>
#include <zircon/compiler.h>

#include "filesystems.h"
#include "misc.h"

bool TestRenameBasic() {
  BEGIN_TEST;
  // Cannot rename when src does not exist
  ASSERT_EQ(rename("::alpha", "::bravo"), -1, "");

  // Renaming to self is fine
  ASSERT_EQ(mkdir("::alpha", 0755), 0, "");
  ASSERT_EQ(rename("::alpha", "::alpha"), 0, "");
  ASSERT_EQ(rename("::alpha/.", "::alpha/."), 0, "");
  ASSERT_EQ(rename("::alpha/", "::alpha"), 0, "");
  ASSERT_EQ(rename("::alpha", "::alpha/"), 0, "");
  ASSERT_EQ(rename("::alpha/", "::alpha/"), 0, "");
  ASSERT_EQ(rename("::alpha/./../alpha", "::alpha/./../alpha"), 0, "");

  // Cannot rename dir to file
  int fd = open("::bravo", O_RDWR | O_CREAT | O_EXCL, 0644);
  ASSERT_GT(fd, 0, "");
  ASSERT_EQ(close(fd), 0, "");
  ASSERT_EQ(rename("::alpha", "::bravo"), -1, "");
  ASSERT_EQ(unlink("::bravo"), 0, "");

  // Rename dir (dst does not exist)
  ASSERT_EQ(rename("::alpha", "::bravo"), 0, "");
  ASSERT_EQ(mkdir("::alpha", 0755), 0, "");
  // Rename dir (dst does exist)
  ASSERT_EQ(rename("::bravo", "::alpha"), 0, "");

  // Rename file (dst does not exist)
  fd = open("::alpha/charlie", O_RDWR | O_CREAT | O_EXCL, 0644);
  ASSERT_GT(fd, 0, "");
  ASSERT_EQ(rename("::alpha/charlie", "::alpha/delta"), 0, "");
  // File rename to self
  ASSERT_EQ(rename("::alpha/delta", "::alpha/delta"), 0, "");
  // Not permitted with trailing '/'
  ASSERT_EQ(rename("::alpha/delta", "::alpha/delta/"), -1, "");
  ASSERT_EQ(rename("::alpha/delta/", "::alpha/delta"), -1, "");
  ASSERT_EQ(rename("::alpha/delta/", "::alpha/delta/"), -1, "");
  ASSERT_EQ(close(fd), 0, "");

  // Rename file (dst does not exist)
  fd = open("::alpha/charlie", O_RDWR | O_CREAT | O_EXCL, 0644);
  ASSERT_GT(fd, 0, "");
  ASSERT_EQ(rename("::alpha/delta", "::alpha/charlie"), 0, "");
  ASSERT_EQ(close(fd), 0, "");

  // Rename to different directory
  ASSERT_EQ(mkdir("::bravo", 0755), 0, "");
  ASSERT_EQ(rename("::alpha/charlie", "::charlie"), 0, "");
  ASSERT_EQ(rename("::charlie", "::alpha/charlie"), 0, "");
  ASSERT_EQ(rename("::bravo", "::alpha/bravo"), 0, "");
  ASSERT_EQ(rename("::alpha/charlie", "::alpha/bravo/charlie"), 0, "");

  // Cannot rename directory to subdirectory of itself
  ASSERT_EQ(rename("::alpha", "::alpha/bravo"), -1, "");
  ASSERT_EQ(rename("::alpha", "::alpha/bravo/charlie"), -1, "");
  ASSERT_EQ(rename("::alpha", "::alpha/bravo/charlie/delta"), -1, "");
  ASSERT_EQ(rename("::alpha", "::alpha/delta"), -1, "");
  ASSERT_EQ(rename("::alpha/bravo", "::alpha/bravo/charlie"), -1, "");
  ASSERT_EQ(rename("::alpha/bravo", "::alpha/bravo/charlie/delta"), -1, "");
  // Cannot rename to non-empty directory
  ASSERT_EQ(rename("::alpha/bravo/charlie", "::alpha/bravo"), -1, "");
  ASSERT_EQ(rename("::alpha/bravo/charlie", "::alpha"), -1, "");
  ASSERT_EQ(rename("::alpha/bravo", "::alpha"), -1, "");

  // Clean up
  ASSERT_EQ(unlink("::alpha/bravo/charlie"), 0, "");
  ASSERT_EQ(unlink("::alpha/bravo"), 0, "");
  ASSERT_EQ(unlink("::alpha"), 0, "");

  END_TEST;
}

bool TestRenameWithChildren() {
  BEGIN_TEST;

  ASSERT_EQ(mkdir("::dir_before_move", 0755), 0, "");
  ASSERT_EQ(mkdir("::dir_before_move/dir1", 0755), 0, "");
  ASSERT_EQ(mkdir("::dir_before_move/dir2", 0755), 0, "");
  ASSERT_EQ(mkdir("::dir_before_move/dir2/subdir", 0755), 0, "");
  int fd = open("::dir_before_move/file", O_RDWR | O_CREAT, 0644);
  ASSERT_GT(fd, 0, "");

  const char file_contents[] = "This should be in the file";
  ASSERT_STREAM_ALL(write, fd, (uint8_t*)file_contents, strlen(file_contents));

  ASSERT_EQ(rename("::dir_before_move", "::dir"), 0, "Could not rename");

  // Check that the directory layout has persisted across rename
  expected_dirent_t dir_contents[] = {
      {false, ".", DT_DIR},
      {false, "dir1", DT_DIR},
      {false, "dir2", DT_DIR},
      {false, "file", DT_REG},
  };
  ASSERT_TRUE(check_dir_contents("::dir", dir_contents, fbl::count_of(dir_contents)), "");
  expected_dirent_t dir2_contents[] = {
      {false, ".", DT_DIR},
      {false, "subdir", DT_DIR},
  };
  ASSERT_TRUE(check_dir_contents("::dir/dir2", dir2_contents, fbl::count_of(dir2_contents)), "");

  // Check the our file data has lasted (without re-opening)
  ASSERT_TRUE(check_file_contents(fd, (uint8_t*)file_contents, strlen(file_contents)), "");

  // Check the our file data has lasted (with re-opening)
  ASSERT_EQ(close(fd), 0, "");
  fd = open("::dir/file", O_RDONLY, 06444);
  ASSERT_GT(fd, 0, "");
  ASSERT_TRUE(check_file_contents(fd, (uint8_t*)file_contents, strlen(file_contents)), "");
  ASSERT_EQ(close(fd), 0, "");

  // Clean up
  ASSERT_EQ(unlink("::dir/dir1"), 0, "");
  ASSERT_EQ(unlink("::dir/dir2/subdir"), 0, "");
  ASSERT_EQ(unlink("::dir/dir2"), 0, "");
  ASSERT_EQ(unlink("::dir/file"), 0, "");
  ASSERT_EQ(unlink("::dir"), 0, "");

  END_TEST;
}

bool TestRenameAbsoluteRelative() {
  BEGIN_TEST;

  char cwd[PATH_MAX];
  ASSERT_NONNULL(getcwd(cwd, sizeof(cwd)), "");

  // Change the cwd to a known directory
  ASSERT_EQ(mkdir("::working_dir", 0755), 0, "");
  DIR* dir = opendir("::working_dir");
  ASSERT_NONNULL(dir, "");
  ASSERT_EQ(chdir("::working_dir"), 0, "");

  // Make a "foo" directory in the cwd
  int fd = dirfd(dir);
  ASSERT_NE(fd, -1, "");
  ASSERT_EQ(mkdirat(fd, "foo", 0755), 0, "");
  expected_dirent_t dir_contents_foo[] = {
      {false, ".", DT_DIR},
      {false, "foo", DT_DIR},
  };
  ASSERT_TRUE(fcheck_dir_contents(dir, dir_contents_foo, fbl::count_of(dir_contents_foo)), "");

  // Rename "foo" to "bar" using mixed paths
  ASSERT_EQ(rename("::working_dir/foo", "bar"), 0, "Could not rename foo to bar");
  expected_dirent_t dir_contents_bar[] = {
      {false, ".", DT_DIR},
      {false, "bar", DT_DIR},
  };
  ASSERT_TRUE(fcheck_dir_contents(dir, dir_contents_bar, fbl::count_of(dir_contents_bar)), "");

  // Rename "bar" back to "foo" using mixed paths in the other direction
  ASSERT_EQ(rename("bar", "::working_dir/foo"), 0, "Could not rename bar to foo");
  ASSERT_TRUE(fcheck_dir_contents(dir, dir_contents_foo, fbl::count_of(dir_contents_foo)), "");

  ASSERT_EQ(rmdir("::working_dir/foo"), 0, "");

  // Change the cwd back to the original, whatever it was before
  // this test started
  ASSERT_EQ(chdir(cwd), 0, "Could not return to original cwd");

  ASSERT_EQ(rmdir("::working_dir"), 0, "");
  ASSERT_EQ(closedir(dir), 0, "");

  END_TEST;
}

bool TestRenameAt() {
  BEGIN_TEST;

  ASSERT_EQ(mkdir("::foo", 0755), 0, "");
  ASSERT_EQ(mkdir("::foo/baz", 0755), 0, "");
  ASSERT_EQ(mkdir("::bar", 0755), 0, "");

  // Normal case of renameat, from one directory to another
  int foofd = open("::foo", O_RDONLY | O_DIRECTORY, 0644);
  ASSERT_GT(foofd, 0, "");
  int barfd = open("::bar", O_RDONLY | O_DIRECTORY, 0644);
  ASSERT_GT(barfd, 0, "");

  ASSERT_EQ(renameat(foofd, "baz", barfd, "zab"), 0, "");

  expected_dirent_t empty_contents[] = {
      {false, ".", DT_DIR},
  };
  ASSERT_TRUE(check_dir_contents("::foo", empty_contents, fbl::count_of(empty_contents)), "");
  expected_dirent_t contains_zab[] = {
      {false, ".", DT_DIR},
      {false, "zab", DT_DIR},
  };
  ASSERT_TRUE(check_dir_contents("::bar", contains_zab, fbl::count_of(contains_zab)), "");

  // Alternate case of renameat, where an absolute path ignores
  // the file descriptor.
  //
  // Here, barfd is used (in the first argument) but ignored (in the second argument).
  ASSERT_EQ(renameat(barfd, "zab", barfd, "::foo/baz"), 0, "");
  expected_dirent_t contains_baz[] = {
      {false, ".", DT_DIR},
      {false, "baz", DT_DIR},
  };
  ASSERT_TRUE(check_dir_contents("::foo", contains_baz, fbl::count_of(contains_baz)), "");
  ASSERT_TRUE(check_dir_contents("::bar", empty_contents, fbl::count_of(empty_contents)), "");

  // The 'absolute-path-ignores-fd' case should also work with invalid fds.
  ASSERT_EQ(renameat(-1, "::foo/baz", -1, "::bar/baz"), 0, "");
  ASSERT_TRUE(check_dir_contents("::foo", empty_contents, fbl::count_of(empty_contents)), "");
  ASSERT_TRUE(check_dir_contents("::bar", contains_baz, fbl::count_of(contains_baz)), "");

  // However, relative paths should not be allowed with invalid fds.
  ASSERT_EQ(renameat(-1, "baz", foofd, "baz"), -1, "");
  ASSERT_EQ(errno, EBADF, "");

  // Additionally, we shouldn't be able to renameat to a file.
  int fd = openat(barfd, "filename", O_CREAT | O_RDWR | O_EXCL);
  ASSERT_GT(fd, 0, "");
  ASSERT_EQ(renameat(foofd, "baz", fd, "baz"), -1, "");
  // NOTE: not checking for "ENOTDIR", since ENOTSUPPORTED might be returned instead.

  // Clean up
  ASSERT_EQ(close(fd), 0, "");
  ASSERT_EQ(unlink("::bar/filename"), 0, "");
  ASSERT_EQ(rmdir("::bar/baz"), 0, "");
  ASSERT_EQ(close(foofd), 0, "");
  ASSERT_EQ(close(barfd), 0, "");
  ASSERT_EQ(rmdir("::foo"), 0, "");
  ASSERT_EQ(rmdir("::bar"), 0, "");
  END_TEST;
}

// Rename using the raw FIDL interface.
bool TestRenameRaw() {
  BEGIN_TEST;

  ASSERT_EQ(mkdir("::alpha", 0755), 0, "");
  ASSERT_EQ(mkdir("::alpha/bravo", 0755), 0, "");
  ASSERT_EQ(mkdir("::alpha/bravo/charlie", 0755), 0, "");

  fbl::unique_fd fd(open("::alpha", O_RDONLY | O_DIRECTORY, 0644));
  ASSERT_TRUE(fd);
  fzl::FdioCaller caller(std::move(fd));

  zx_status_t status;
  zx::handle token;
  ASSERT_EQ(
      fuchsia_io_DirectoryGetToken(caller.borrow_channel(), &status, token.reset_and_get_address()),
      ZX_OK);
  ASSERT_EQ(status, ZX_OK);

  // Pass a path, instead of a name, to rename.
  // Observe that paths are rejected.
  constexpr char src[] = "bravo/charlie";
  constexpr char dst[] = "bravo/delta";
  ASSERT_EQ(fuchsia_io_DirectoryRename(caller.borrow_channel(), src, strlen(src), token.release(),
                                       dst, strlen(dst), &status),
            ZX_OK);
  ASSERT_EQ(status, ZX_ERR_INVALID_ARGS);

  // Clean up
  ASSERT_EQ(unlink("::alpha/bravo/charlie"), 0, "");
  ASSERT_EQ(unlink("::alpha/bravo"), 0, "");
  ASSERT_EQ(unlink("::alpha"), 0, "");

  END_TEST;
}

RUN_FOR_ALL_FILESYSTEMS(rename_tests,
                        RUN_TEST_MEDIUM(TestRenameBasic) RUN_TEST_MEDIUM(TestRenameWithChildren)
                            RUN_TEST_MEDIUM(TestRenameAbsoluteRelative)
                                RUN_TEST_MEDIUM(TestRenameAt) RUN_TEST_MEDIUM(TestRenameRaw))
